<?php
###########################################################################
#  Copyright (C) 2006-2007 Glyphix, Inc. <briz@glyphix.com>               #
#                                                                         #
#  Permission is hereby granted, free of charge, to any person obtaining  #
#  a copy of this software and associated documentation files (the        #
#  "Software"), to deal in the Software without restriction, including    #
#  without limitation the rights to use, copy, modify, merge, publish,    #
#  distribute, sublicense, and/or sell copies of the Software, and to     #
#  permit persons to whom the Software is furnished to do so, subject to  #
#  the following conditions:                                              #
#                                                                         #
#  The above copyright notice and this permission notice shall be         #
#  included in all copies or substantial portions of the Software.        #
#                                                                         #
#  Except as contained in this notice, the name(s) of the above           #
#  copyright holders shall not be used in advertising or otherwise to     #
#  promote the sale, use or other dealings in this Software without       #
#  prior written authorization.                                           #
#                                                                         #
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        #
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     #
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
#  IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR      #
#  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,  #
#  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#  OTHER DEALINGS IN THE SOFTWARE.                                        #
###########################################################################

/**
 * GXPage_Action
 *
 * Executes an <Action> node. Actions are embedded within <Page> nodes.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @subpackage	Engine
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 * @uses		GXPage_SubClass
 */

class GXPage_Action extends GXPage_SubClass {

	// the page called
	public $Page;
	// the action called
	public $Action;

	// Must instantiate this class with the Realm/Action you want called, it will return itself
	public function __construct( $Page = null, $Action = null ){
		parent::__construct();
		if( !$Page || !$Action ){
			trigger_error( 'Missing Page or Action in __construct(): ' .$Page .' / ' .$Action, E_USER_ERROR );
			return false;
		}

		if( !($ActionConf = GXPage::get('SiteConfig')->getAction($Page,$Action)) ){
			trigger_error( 'Unable to locate the requested action (' .$Page .'/' .$Action .')', E_USER_ERROR );
			return false;
		}
		$this->Page = $Page;
		$this->Action = $Action;

		// holds roles
		$roles = array();
		// holds Args, to be collected below
		$Args = array();

		// hold the Call node
		$call = null;
		// Check for absolutely necessary nodes
		if( !($call = $ActionConf->exec->xpath('Call',0)) ){
			trigger_error( 'Action ' .$Page .'/' .$Action .' is not valid (requires a Call node)', E_USER_ERROR );
			return false;
		}

		// store the Call parameters
		$callParams = $call->exec->xpath('*');

		// Collate roles
		if( $auth = $ActionConf->exec->xpath('Auth/*') ){
			foreach( $auth as $role ){
				$roles[] = $role->attributes['ID'];
			}
		}

		// convert to simplexml
		$ActionConf = simplexml_import_dom(clone $ActionConf->node);

		// Add attributes
		$Config['Page']			= $Page;
		$Config['Action']		= $Action;
		$Config['Output']		= isset($ActionConf->Output) ? strval($ActionConf->Output) : null;
		// transform action result by default
		$Config['Transform']	= isset($ActionConf->Output['Transform']) ? strval($ActionConf->Output['Transform']) : null;
		// show runtime by default ... turn it off with a zero
		$Config['Runtime']		= isset($ActionConf->Output['Runtime']) ? strval($ActionConf->Output['Runtime']) : null;
		// strip the doctype header line
		$Config['StripDoctype']	= strval($ActionConf->Output['StripDoctype']);
		$Config['Object'] 		= strval($ActionConf->Call['Object']);
		$Config['Method']		= strval($ActionConf->Call['Method']);
		$Config['Replace']		= strval($ActionConf->Replace);
		$Config['File']			= strval($ActionConf->File);
		$Config['onSuccess']	= array(
			'Page'		=> strval($ActionConf->onSuccess['Page']),
			'Action'	=> strval($ActionConf->onSuccess['Action']),
			'Redirect'	=> strval($ActionConf->onSuccess['Redirect']),
			'Protocol'	=> strval($ActionConf->onSuccess['Protocol'])
			);
		$Config['onFailure']	= array(
			'Page'		=> strval($ActionConf->onFailure['Page']),
			'Action'	=> strval($ActionConf->onFailure['Action']),
			'Redirect'	=> strval($ActionConf->onFailure['Redirect']),
			'Protocol'	=> strval($ActionConf->onFailure['Protocol'])
			);
		// top-level vars to be used the same for onSuccess or onFailure
		$Config['toPage']			= null;
		$Config['toAction']		= null;
		// holds the roles allowed for this action
		$Config['Roles']		= $roles;
		// <Theme> and <StyleSheet> on an action can override the <Page> values for those elements
		$Config['Theme']		= strval($ActionConf->Theme);
		$Config['StyleSheet']	= strval($ActionConf->StyleSheet);
		$Config['Args']			= null;
		$Config['Result']		= 'OK';

		// if site.xml is set up incorrectly, we might not have onSuccess or onFailure, so set them to the first <Page>
		if( !$Config['onSuccess']['Page'] ){
			$Config['onSuccess']['Page'] = $Page;
		}
		if( !$Config['onFailure']['Page'] ){
			$Config['onFailure']['Page'] = $Page;
		}

		// build the call parameters
		if( $callParams ){
			// collect args
			foreach( $callParams as $Param ){
				$Args[ $Param->nodeName ] = array(
					'Required'	=> $Param->attributes['Required'],
					'Value'		=> $Param->nodeValue
					);
			}
			// load values into system args
			$Config['Args'] = $this->genArgs($Args);
			// load this item's params
			GXPage::get('Params')->loadCallerParams($call);
		}

		// store the raw array
		$this->setConfig($Config);

		// now make the call
		$objName = $Config['Object'];
		// instantiate the object ... last parameter forces object to load
		$obj = GXObject::get( $Config['Object'], null, null, true );
		// variable to hold the results
		$doc = null;
		// if error, return the handleError() method
		trigger_error( 'Calling ' .get_class($obj) .'::' .$Config['Method'] .' with ' .print_r($Config['Args'],true), E_USER_WARNING );
		if( !($doc = GXObject::run( $obj, $Config['Method'], $Config['Args'] )) ){
			trigger_error( 'Error calling action ' .$Page .'/' .$Action, E_USER_ERROR );
			$this->getConfig()->exec->xpath('Result',0)->nodeValue = 'ERROR';
			$Config['Result'] = 'ERROR';
			return $this->onFailure();
		}
		// return must be string or valid object types
		$valid = array('GXDOM','DOMDocument','DOMElement','SimpleXML','SimpleXMLElement','GXPage_Page','GXPage_Action');
		if( !(gettype($doc) == 'string' || (gettype($doc) == 'boolean' && $doc) || in_array(get_class($doc),$valid)) ){
			trigger_error( 'Your Call (' .$Page .'/' .$Action .') must return true, a valid object, or a string', E_USER_NOTICE );
			$this->getConfig()->exec->xpath('Result',0)->nodeValue = 'ERROR';
			return $this->onFailure();
		}
		// if we have a GXPage_Page or GXPage_Action object, get its result
		if( is_object($doc) && (get_class($doc) == 'GXPage_Page' || get_class($doc) == 'GXPage_Action') ){
			$doc = $doc->getResult();
		}

		// if we made it this far, set the doc
		$this->result = $doc;

		// if the instantiated object has a Page val, use it for onSuccess
		if( isset($obj->Page) ){
			$ActionConf->onSuccess->Page = $obj->Page;
		}

		return $this->onSuccess();
	}

	/************************************************
	 *                 public methods               *
	 ************************************************/

	/**
	 * processResult
	 *
	 * Uses this object's config to determine whether to call onSuccess or onFailure,
	 * as well as whether to redirect.
	 * @return ...
	 */
	public function processResult(){
		// if we're not OK, use onFailure; else use onSuccess
		$what = ($this->Result != 'OK') ? $this->onFailure : $this->onSuccess;
		trigger_error( 'Action (' .$this->Page .'/' .$this->Action .') result: ' .$this->Result .'; returning page ' .$what['Page'], E_USER_WARNING );

		// redirect from here
		if( $what['Redirect'] ){
			trigger_error( $this->Page .'/' .$this->Action .' requests a redirect, redirecting to ' .$what['Page'], E_USER_WARNING );
			// if we have a protocol, build an entire absolute url
			$self = isset($_REQUEST['gxpage-params']) ? null : GXPage::get('Layout')->Client->Self .'/';
			header( 'Location: ' .( ($what['Protocol']) ? $what['Protocol'] .'://' .GXPage::get('Run')->Host : null ) .GXPage::get('Layout')->Client->Root .$self .$what['Page'] );
			exit();
		}
	}

	/**
	 * getDestinationPage
	 *
	 * Returns the toPage value, or the onSuccess/onFailure Page value.
	 * @return string
	 */
	public function getDestinationPage(){
		$what = ($this->Result != 'OK') ? $this->onFailure : $this->onSuccess;
		return (($this->toPage) ? $this->toPage : $what['Page']);
	}

	/**
	 * getTransform
	 *
	 * Returns boolean true if the action is supposed to be transformed.
	 * Returns boolean false if it's not supposed to be transformed.
	 * If the action has no Transform attribute, it returns the getTransform method from SiteConfig.
	 * @return boolean
	 */
	public function getTransform(){
		return (is_null($this->Transform) ? GXPage::get('SiteConfig')->getTransform() : $this->Transform );
	}

	/**
	 * getOutput
	 *
	 * Returns the Output value for this action.
	 * The constructor set it to null if there isn't one.
	 * @return mixed
	 */
	public function getOutput(){
		return (is_null($this->Output) ? GXPage::get('SiteConfig')->getOutput() : $this->Output);
	}

	/**
	 * getFile
	 *
	 * Returns the File value for this action.
	 * The constructor set it to null if there isn't one.
	 * @return mixed
	 */
	public function getFile(){
		return $this->File;
	}

	/**
	 * getRuntime
	 *
	 * Returns boolean true if the action is supposed to include gxpage-runtime.
	 * Returns boolean false if it's not supposed to include gxpage-runtime.
	 * If the action has no Runtime attribute, it returns the getRuntime method from SiteConfig.
	 * @return boolean
	 */
	public function getRuntime(){
		return (is_null($this->Runtime) ? GXPage::get('SiteConfig')->getRuntime() : $this->Runtime );
	}

	/************************************************
	 *                internal methods              *
	 ************************************************/

	/**
	 * genArgs
	 *
	 * Assigns a value for each argument required by <Action>.
	 * @param array $Args key=>val pairs
	 * @return array $list or false on failure
	 */
	private function genArgs( $Args = null ){
		$list = array();
		// if we don't have an array, there are no args
		if( is_array($Args) && count($Args) ){

			// collect everything
			$SysArgs = GXPage::get('Params')->collect();

			// determine the value for each arg
			foreach( $Args as $Key => $conf ){

				// check in SysArgs, fall back to $_REQUEST
				$val = (isset($SysArgs[$Key])) ? $SysArgs[$Key] : ((isset($_REQUEST[$Key])) ? $_REQUEST[$Key] : $conf['Value']);

				// spit out the value we obtained for each arg
				trigger_error( 'Action arg: ' .$Key .' = ' .print_r($val, true), E_USER_WARNING );

				// if an arg is required and not available, stop processing
				if( $conf['Required'] && (gettype($val) == 'string' && strlen($val) == 0) ){
					trigger_error( 'Arg required but not available: ' .$Key .'=[' .$val .']', E_USER_WARNING );
					return false;
				}

				// add the arg
				$list[$Key] = $val;
			}
		}
		return $list;
	}

	/**
	 * onSuccess
	 *
	 * Handle successful execution of the action.
	 * This includes messing with various parameters depending on the output value.
	 * @return object $this
	 */
	private function onSuccess(){
		switch(true){
			/* ... moved the generation of xml string to GXOutput so that we can force XML to download as a file.
			// if output is set to File and we have GXDOM, unset output to let gxpage handle it automatically
			case strtolower($this->Output) == 'file' && get_class($this->getResult()) == 'GXDOM':
				$this->Output = null;
				break;
				*/

			// if output is set to File, unset File, Replace and StyleSheet
			case strtolower($this->Output) == 'file':
				$this->File = null;
				$this->Replace = null;
				//$this->StyleSheet = null;
				break;
		}

		// if we have an Output directive, return the result
		if( $this->Output ){
			return $this->result;
		}

		// determine if we need to recurse;
		// if so, return a new GXPage_Action, which will in effect follow all the other actions up the chain
		$success = $this->onSuccess;
		if( $success['Page'] && $success['Action'] ){
			return new GXPage_Action( $success['Page'], $success['Action'] );
		}

		// if we have a special character in Page, use the last page we displayed
		if( substr($success['Page'],0,1) == '@' ){
			$this->toPage = GXPage::get('Run')->From;
		}

		// done
		return $this;
	}

	/**
	 * onFailure
	 *
	 * If <Action> has onFailure, load that page.
	 * @return object $this
	 */
	private function onFailure(){
		$fail = $this->onFailure;
		$toPage = $fail['Page'];
		if( !$toPage ){
			// this will return the xpath of this using the magic __call method of the parent class
			$toPage = $this->{'//Page'};
		}
		// unset Page, File and Output
		$this->Page = null;
		$this->File = null;
		$this->Output = null;
		$this->Result = 'ERROR';
		trigger_error( 'Action failed (' .$this->Page .'/' .$this->Action .')', E_USER_ERROR );
		return $this;
	}

}

?>
